Skip to content

feat(plugin-security): walled bootstrap stops minting the platform-admin grant row; platformAdmin audit service; legacy-grant deprecation pointer (L4) - #13514

Draft
os-steve wants to merge 1 commit into
mainfrom
claude/issue-11974-l4-plugin-security-walled-bootstrap
Draft

feat(plugin-security): walled bootstrap stops minting the platform-admin grant row; platformAdmin audit service; legacy-grant deprecation pointer (L4)#13514
os-steve wants to merge 1 commit into
mainfrom
claude/issue-11974-l4-plugin-security-walled-bootstrap

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #11974

Part of #11663 (leg L4; design comment 5394453215 §6 row L4, maintainer acceptance 5404675670, bundle 1A/2B/3A/4A/5A/6A/7A — verbatim 「接受你的建议,继续」). #11973 (L3) remains open and is not addressed here; #11670 remains open.

What lands

Union of gates/tests re-run at the final commit d5eeda5 — all green (15 derived gate families incl. check:nul-bytes, plugin-security 90 test files / 1664 tests, package typecheck; downstream: verify harness posture suites 18/18, walled dogfood fixtures 18 passed / 3 env-skipped).

  1. Walled postures stop minting the grant row. bootstrapPlatformAdmin under group/isolated writes no sys_user_permission_set row, whatever accounts exist. Standing is config-derived at the one derivation site (resolve-authz-context.ts §6b-config, landed with L2/PR feat(core): anchor PLATFORM_ADMIN on a verified OS_PLATFORM_OWNER_EMAIL match, inside the one derivation site #13146 and fix(core,plugin-auth,plugin-security): every OS_PLATFORM_OWNER_EMAIL reader asks the ONE list-aware parser #13319) — each declared, verified account resolves PLATFORM_ADMIN at request time. single keeps first-user promotion and its grant row byte-for-byte (Choice 4A; over-denial pins kept).
  2. Read-only platformAdmin service (new platform-admin-service.ts, registered by SecurityPlugin): configuredEmails() (declared/refused/normalized list) + standing() (per entry: registered, verified, standing-holding user id). Frozen; no writable member (Choice 3A: no runtime mutation path). The walled bootstrap's per-boot standing log consumes the same implementation, so log and service cannot disagree.
  3. Deprecation log fires at this leg (pin [WIP] Fix error in step four of the action run #5). A detected legacy org-less human grant is pointed at the config path through core's reportLegacyPlatformAdminGrant — the SAME once-per-process latch the derivation-site reporter uses, so boot-time + request-time detection can never total two lines. Pinned: a seeded legacy grant produces exactly one line naming OS_PLATFORM_OWNER_EMAIL, the holder id and their address, across repeated bootstraps. Nothing is revoked (P5's honoured window).
  4. Explain agrees with enforcement (P8): new pin — a declared+verified admin with ZERO grant rows explains as PLATFORM_ADMIN through buildContextForUser (which delegates to the one resolver; core's own §6b-config pins cover the derivation side).
  5. Replay narrowed. shouldReplayBootstrapFor now fires only for sys_user insert/create under non-walled postures. The [security] 围墙提权只按未验证邮箱匹配 —— #11184 落地后仍存在一条更窄的同形路径:抢在 owner 之前用其邮箱注册即可被提权 #11343 update arm (email_verified/email) existed solely to re-attempt the walled elevation after the verifying write; that elevation is retired, and under walled postures no sys_user write can change the bootstrap's answer. Middleware-seam pin re-derived accordingly.
  6. The one-line local isEmailVerified alias is deleted (the predicate's canonical home is @objectstack/types, consumed via the shared standing implementation — the card's "promoted into core" premise was stale; nothing in packages/types or packages/core is touched).

Guard refusals: none deleted, none added

L3's last-admin-guard re-pricing is its own reviewed step and is untouched (as is all of packages/plugins/plugin-auth/**). The walled walled_owner_email_undeclared backstop refusal is kept with its exact reason string; walled_owner_not_registered / walled_owner_not_verified are not refusals removed but elevation outcomes whose decision moved to the derivation site — their distinctions survive as the standing report's per-entry registered/verified fields, and the fail-closed direction (unverified confers nothing) is enforced and pinned in core.

Behavior notes for the contract reviewer

  • Bootstrap return contract: under walled postures with a usable config the reason is now walled_config_derived (was walled_owner_not_registered/walled_owner_not_verified, or a grant write). already_have_admin is now a single-only outcome; under walled a pre-existing human grant row gets the deprecation pointer instead of a silent early exit. The only in-repo consumer reading the report (cli meta resync) reads seed/resync counters only — unaffected.
  • Seed-ownership handoff (claimSeedOwnership) no longer runs under walled postures — it was keyed on the promoted admin, and there is none. Walled/multi-org seed ownership is the enterprise organizations package's per-org bootstrap job (per auth-plugin.ts's posture split). single handoff unchanged.
  • Reader census for the stopped write (PM assumption 3, measured at 3800e42): plugin-auth's ensureDefaultOrganization is gated !postureEnforcesWall(...) and never runs under walled postures in this repo; last-admin-guard sees the same zero-grant-row population a fresh walled rig already has today before its owner registers (no new state); explain's provenance reads report nothing on empty tables; auto-org-admin-grant reads the permission-set rows, which this leg keeps minting (the org-less-row reap is L6, behind the census and auto-org-admin-grant resolves the organization_admin set id by name alone (limit 1, unscoped, process-cached), so walled org-admin grants can point at the organization-less row #11670's resolver scoping). Nothing revoked on existing rigs — the legacy read is honoured.
  • Known interim window (design H4, owned by L3 platform-admin re-anchor L3 (plugin-auth): re-point ensure-default-organization; re-price last-admin-guard as its own reviewed step #11973 — not addressed here): the enterprise organizations package (out of this repo) reuses ensureDefaultOrganization, which still finds "the platform admin" via grant rows and triggers on grant inserts. A FRESH walled rig running that package will not auto-create its default organization between this leg landing and L3's re-point. The design's §6 dependency table imposes no L3-before-L4 order (both depend on L2 only), and existing rigs keep their rows; flagged here so the review weighs it consciously.
  • The harness fixture item from the card was measured dead: walled harness rigs declare the dev admin as owner and the dev-admin seed already stamps email_verified: true, so config-derived standing works with zero fixture changes (packages/verify/src/harness.ts untouched).

Generated by Claude Code


Generated by Claude Code

…min grant row; platformAdmin audit service; legacy-grant deprecation pointer

Under walled postures bootstrapPlatformAdmin writes no sys_user_permission_set
row any more - standing is config-derived at the one derivation site
(resolve-authz-context 6b-config). single keeps first-user promotion and its
grant row byte-for-byte (Choice 4A). The walled branch now reports: a per-boot
standing log (one implementation with the new read-only platformAdmin service:
configuredEmails() + standing()), and a once-per-process deprecation pointer
for a detected legacy org-less grant, through the same latch the derivation-
site reporter uses, naming OS_PLATFORM_OWNER_EMAIL and the holder.

shouldReplayBootstrapFor narrows with the retired elevation: sys_user
insert/create under non-walled postures only - the #11343 update arm existed
solely to re-attempt the walled elevation after the verifying write, and under
walled postures no sys_user write can change the bootstrap's answer at all.

The local isEmailVerified alias is gone; the shared predicates
(isEmailVerifiedUserRow, normalizePlatformAdminEmail) are consumed directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 30 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/plugins/plugin-security/src/index.ts, packages/plugins/plugin-security/src/platform-owner-wall-bypass.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

29 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json e7191ce71da1a8cc25c05e3d372eb4f63be5517a.

5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/plugins/plugin-security/src/index.ts, packages/plugins/plugin-security/src/platform-owner-wall-bypass.ts) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: sys_user (literal, 29 pages)
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e7191ce71da1a8cc25c05e3d372eb4f63be5517apackageMentionDocs.

Which tree this was computed on

This run read content/docs from 9fb9e8d89cf2707c3b7f29eac7ca0814f9f7ee1f — the merge of head d5eeda5e408cd10391e48f7a1613c39c7d640f9a into base e7191ce71da1a8cc25c05e3d372eb4f63be5517a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9fb9e8d89cf2707c3b7f29eac7ca0814f9f7ee1f && git checkout 9fb9e8d89cf2707c3b7f29eac7ca0814f9f7ee1f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e7191ce71da1a8cc25c05e3d372eb4f63be5517a d5eeda5e408cd10391e48f7a1613c39c7d640f9a && git checkout -B drift-repro e7191ce71da1a8cc25c05e3d372eb4f63be5517a && git merge --no-ff d5eeda5e408cd10391e48f7a1613c39c7d640f9a

node scripts/docs-audit/affected-docs.mjs --json e7191ce71da1a8cc25c05e3d372eb4f63be5517a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs e7191ce71da1a8cc25c05e3d372eb4f63be5517a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation needs:contract-review tests tooling

Projects

None yet

2 participants